#!/usr/bin/python

import curses
from curses import panel
from curses.textpad import Textbox
import _curses
import sys
curses.KEY_ESC = 27
curses.KEY_ENTER2 = ord('\n')

class BaseDialog():
    default_h, default_w = 10, 36
    def __init__(self, **options):
        self.title_attr = options.get(
            'title_attr',
            curses.A_NORMAL | curses.A_STANDOUT | curses.color_pair(0)
        )
        self.msg_attr   = options.get('msg_attr',   curses.A_NORMAL)
        self.opt_attr   = options.get('opt_attr',   curses.A_NORMAL)
        self.focus_attr = options.get('focus_attr', curses.A_BOLD | curses.color_pair(5))
        self.title      = options.get('title', '')
        self.message    = options.get('message', '')
        self.width      = self.get_width(
            options.get('width', self.default_w)
        )
        self.height     = self.get_height(
            options.get('height', self.default_h)
        )
        self.max_lines  = self.height - 4
        self.top = int((curses.LINES - self.height) / 2)
        self.left = int((curses.COLS - self.width) / 2)

        self.win = curses.newwin(
            self.height, self.width, self.top, self.left
        )
        self.win.box()
        self.y, self.x = self.win.getmaxyx()

        self.win.addstr(0, 0, ' ' * self.width, self.title_attr)
        self.win.keypad(1)
        self.focus = 0
        self.enterKey = False
        self.win.keypad(1)
        #curses.curs_set(0)
        #curses.noecho( )
        #curses.cbreak( )
        self.tmp = 1
    def get_width(self, op_width):
        if op_width == 'auto':
            if self.lines:
                exp_width = 2 + max([len(line) for line in self.lines]) + 2
                return exp_width if exp_width > self.default_w else self.default_w
            return self.default_w
        return op_width
    def get_height(self, op_height):
        if op_height == 'auto':
            exp_height = 1 + len(self.lines) + 3
            return exp_height if exp_height > self.default_h else self.default_h
        return op_height
    def rectangle(self, height, width, ul_y, ul_x, attr):
        lr_y, lr_x = ul_y + height + 1, ul_x + width + 1
        self.win.vline(ul_y, ul_x, curses.ACS_VLINE, height+2, attr)
        self.win.hline(ul_y, ul_x, curses.ACS_HLINE, width+1 , attr)
        self.win.hline(lr_y, ul_x, curses.ACS_HLINE, width+1 , attr)
        self.win.vline(ul_y, lr_x, curses.ACS_VLINE, height+1, attr)

        # lower-right char, when bottom ended but not right ended
        lr_char = curses.ACS_LRCORNER
        if lr_y == self.y - 1 and lr_x < self.x - 1:
            lr_char = curses.ACS_BTEE
        # upper-right char, when right ended
        ur_char = curses.ACS_URCORNER
        if lr_x == self.x - 1:
            ur_char = curses.ACS_RTEE

        if lr_x < self.x - 1:
            self.win.addch(ul_y, lr_x, ur_char,  attr)
            self.win.addch(lr_y, lr_x, lr_char,  attr)
        else:
            self.win.insch(ul_y, lr_x, ur_char,  attr)
            self.win.insch(lr_y, lr_x, lr_char,  attr)

        if lr_y < self.y - 1:
            self.win.addch(lr_y, ul_x,       curses.ACS_LLCORNER,  attr)
        else:
            self.win.addch(lr_y, ul_x,       curses.ACS_BTEE,  attr)
        self.win.addch(ul_y,        ul_x,       curses.ACS_ULCORNER,  attr)
        #self.win.addch(height+ul_y, ul_x,       curses.ACS_BTEE,  attr)
        self.win.refresh()

    def button(self, ul_y, ul_x, caption, focused):
        if focused:
            _op = self.opt_attr | self.focus_attr
        else:
            _op = self.opt_attr
        self.rectangle(1, len(caption)+2, ul_y, ul_x, _op &~ curses.A_BOLD)
        self.win.addstr(ul_y + 1, ul_x + 1, ' {0} '.format(caption), _op)

    def left_right_key_event_handler(self, num_of_buttons):
        self.win.refresh()
        key = self.win.getch()
        if key == curses.KEY_LEFT and self.focus != 0:
            self.focus -= 1
        elif key == curses.KEY_RIGHT and self.focus != num_of_buttons - 1:
            self.focus += 1
        elif key == curses.KEY_ENTER2:
            self.enterKey = True
    def doModal(self):
        if self.title:
            self.win.addstr(0, int(self.x/2-len(self.title)/2), self.title, self.title_attr)

        _lines = [l for l in self.lines]
        if len(_lines) > self.max_lines:
            _lines[self.max_lines-1] = "..."

        for (i, msg) in enumerate(_lines[:self.max_lines]):
            self.win.addstr(i+1, 2, msg, self.msg_attr)
    @property
    def lines(self):
        if isinstance(self.message, str):
            _lines = self.message.split('\n')
        elif isinstance(self.message, list):
            _lines = self.message
        else:
            _lines = []
        return _lines
    def clear(self):
        self.win.clear()
        self.win.refresh()
        # no use
        #curses.doupdate()

class MessageDialog(BaseDialog):
    def doModal(self):
        super(MessageDialog, self).doModal()
        ## center button with padding-bottom=1
        #self.button(6, int(self.x/2-3), 'OK', True)
        ## bottom-right  button
        self.button(self.height-3, int(self.x-6), 'OK', True)

        ## win.nodelay(False) is default, OR will cause overflow or freeloop
        ## waiting for ENTER
        self.win.nodelay(False)
        while True:
            key = self.win.getch()
            if key == curses.KEY_ENTER2 or \
               key == curses.KEY_ESC:
                break
        self.clear()

class ConfirmDialog(BaseDialog):
    def doModal(self):
        super(ConfirmDialog, self).doModal()
        #for _op in ['Yes', 'Cancel']:
        while self.enterKey != True:
            if self.focus == 0:
                self.button(7, 4, 'OK', True)
            else:
                self.button(7, 4, 'OK', False)

            if self.focus == 1:
                self.button(7, 12, 'CANCEL', True)
            else:
                self.button(7, 12, 'CANCEL', False)

            self.left_right_key_event_handler(2)
        self.clear()
        if self.focus == 0: return True
        return False

class InputDialog(BaseDialog):
    def doModal_w_getstr(self):
        ''' this method is obsoleted '''
        _rst = ''
        if super(InputDialog, self).doModal():
            curses.echo()
            curses.curs_set(1)
            self.win.keypad(False)
            self.win.addstr(5, 2, 'Please enter:')
            self.win.addstr(6, 2, ' '*30, curses.A_UNDERLINE)
            _rst = self.win.getstr(6, 2)
            ## for python3, type of _rst is bytes
            if not isinstance(_rst, str):
                _rst = _rst.decode(encoding='utf-8')
            curses.curs_set(0)
            curses.noecho()
        self.clear()
        return _rst
    def doModal(self):
        _rst = ''
        super().doModal()

        curses.curs_set(1)
        self.win.addstr(4, 2, 'Please enter:')

        editwin = self.win.subwin(1, self.width-6, self.top+6, self.left+3)
        self.rectangle(1, self.width-6, 5, 2, curses.A_BOLD)

        box = Textbox(editwin)
        # Let the user edit until Ctrl-G is struck.
        box.edit()
        _rst = box.gather()
        ## for python3, type of _rst is bytes
        #_rst = self.win.getstr(6, 2)
        if not isinstance(_rst, str):
            _rst = _rst.decode(encoding='utf-8')
        curses.curs_set(0)
        self.clear()
        return _rst

class Menu():
    def __init__(self, items, stdscreen, top, left, exit_label='exit'):
        self.position = 0
        self.items = items
        self.items.append((exit_label, "exit"))

        h = len(self.items) + 2
        w = max([len(s) for s,t in self.items]) + 4 + 3
        self.window = stdscreen.subwin(h, w, top, left)
        self.window.keypad(1)

        self.panel = panel.new_panel(self.window)
        self.panel.hide()
        panel.update_panels()


    def navigate(self, n):
        self.position += n
        if self.position < 0:
            self.position = 0
        elif self.position >= len(self.items):
            self.position = len(self.items) - 1

    def display(self):
        self.panel.top()
        self.panel.show()
        self.window.clear()
        self.window.box()
        while True:
            self.window.refresh()
            curses.doupdate()
            for index, item in enumerate(self.items):
                if index == self.position:
                    mode = curses.A_REVERSE
                else:
                    mode = curses.A_NORMAL
                msg = "%d. %s" % (index, item[0])
                self.window.addstr(1 + index, 2, msg, mode)
            key = self.window.getch()
            if key in [curses.KEY_ENTER, ord("\n")]:
                if self.position == len(self.items) - 1:
                    break
                self.items[self.position][1]()
            elif key == curses.KEY_UP:
                self.navigate(-1)
            elif key == curses.KEY_DOWN:
                self.navigate(1)
            elif key == curses.KEY_ESC:
                break
        self.window.clear()
        self.panel.hide()
        panel.update_panels()
        curses.doupdate()

class CursesDisplay():
    def __init__(self, _config={}):
        self.scr = curses.initscr()
        # initializes eight basic colors, and two global variables in the curses module, COLORS and COLOR_PAIRS
        self.colors = {}
        if 'colors' in _config:
            curses.start_color()
            try:
                curses.use_default_colors()
            except _curses.error as ex:
                print(ex)
                sys.exit(1)
            if 'default' in _config['colors']:
                # Color pair 0 is hard-wired to white on black, and cannot be changed
                for i in range(0, curses.COLORS):
                    curses.init_pair(i + 1, i, -1)
            else:
                pass
                for c, v in _config['colors'].items():
                    curses.init_pair(c, v[0], v[1])
                    self.colors[c] = i

        self.scr.keypad(True)
        # close echo for input
        curses.noecho()
        # no enter needed for input
        curses.cbreak()
        # getch() will be non-blocking
        self.scr.nodelay(True)
        # hide cursor
        ## get rid of ESC delay
        curses.set_escdelay(25)
        try:
            curses.curs_set(0)
        except _curses.error as ex:
            print(ex)
            sys.exit(1)

    def __del__(self):
        if __name__ == '__main__':
            self.destroy()
    def destroy(self):
        curses.nocbreak()
        self.scr.keypad(False)
        curses.echo()
        curses.endwin()

    @property
    def Lines(self):
        return curses.LINES
    @property
    def Columns(self):
        return curses.COLS
    @property
    def Bottom(self):
        return self.Lines - 1
    @property
    def Right(self):
        return self.Columns - 1

    def charAt(self, row, col, ch):
        try:
            self.scr.addch(row, col, ch)
        except _curses.error as e:
            pass
    def clear(self):
        self.scr.clear()
    ''' getch will implicit call refresh
    '''
    def refresh(self):
        self.scr.refresh()
    def getInput(self):
        self.scr.nodelay(0)
        ch = self.scr.getch()
        self.scr.nodelay(1)
        return ch
    def newwin(self, height, width, begin_y, begin_x):
        return curses.newwin(height, width, begin_y, begin_x)
    def addStrLine(self, _row, _col, _str, _hl=False):
        self.scr.clrtoeol()
        try:
            ### TODO ... <red></red>
            if _hl:
                self.scr.addstr(_row, _col, _str, curses.A_REVERSE)
            else:
                self.scr.addstr(_row, _col, _str)
        except _curses.error as e:
            pass


def test_CursesDisplay(stdscr):

    cd = CursesDisplay({
        'colors': {'default': 256}
    })

    stdscr = cd.scr
    stdscr.addstr(2, 2, 'L:{}, C:{}'.format(
        curses.LINES, curses.COLS
    ))

    ## set boundary and center
    cd.charAt(0, 0, '+')
    cd.charAt(cd.Bottom, 0, '+')
    cd.charAt(0, cd.Right, '+')
    cd.charAt(cd.Bottom, cd.Right, '+')
    cd.charAt(int(cd.Bottom/2), int(cd.Right/2), '+')

    cd.addStrLine(1, int(cd.Right/2)-6, 'Hello Curses!')
    _ovf_test_str = '1234567890abcdefghi!'
    for i in range(2, 5):
        cd.addStrLine(i, cd.Right-14+i, _ovf_test_str)
    #cd.addStrLine(cd.Bottom, cd.Right-8, _ovf_test_str)

#    cd.getInput()

    cols_of_color = 36
    cd.scr.addstr(6, 0, 'COLORS: ' + str(curses.COLORS))
    for i in range(0, curses.COLORS):
        cd.scr.addstr(7, i%cols_of_color, str((i%cols_of_color+1)%10))
    try:
        cd.scr.move(8, 0)
        for i in range(0, 256):
            cd.scr.addch(curses.ACS_BLOCK, curses.color_pair(i))
            if i%cols_of_color == cols_of_color-1:
                cd.scr.move(cd.scr.getyx()[0]+1, 0)
        #cd.scr.bkgd(' ', curses.color_pair(13))
    except _curses.error:
        # End of screen reached
        pass

    def test_dialogs():
        MessageDialog(
            message=['message line 1', 'message line 2'], title='Message Dialog'
        ).doModal()

        if ConfirmDialog(
                message='message test', title='Confirm Dialog'
        ).doModal():
            MessageDialog(message='You entered "OK"').doModal()
        else:
            MessageDialog(message='You entered "Cancel"').doModal()

        _input = InputDialog(message='input prompt', title='Input Box').doModal()
        cd.addStrLine(14, 6, _input)
        MessageDialog(message=_input, title='Result of Last Input box').doModal()

        MessageDialog(
            message=['message1', 'message2'], title='this is a title',
            width=20,
            height=7
        ).doModal()

    def test_menu():
        import functools
        Menu([
            ('Menu', functools.partial(print, 'menu ok'))
        ], cd.scr, 18, 22, 'Back').display()
    ## show a menu
    main_menu = Menu([
        ('Dialog', test_dialogs),
        ('Menu', test_menu),
        ('Menu2-Menu2.2', test_menu)
    ], cd.scr, 17, 5)
    main_menu.display()

if __name__ == '__main__':
    curses.wrapper(test_CursesDisplay)
